public class ElasticSearch笔记 {
/*
分布式搜索引擎-----ElasticSearch

elasticsearch的作用:
elasticsearch是一款非常强大的开源分布式搜索引擎，具备非常多强大功能，可以帮助我们从海量数据中快速找到需要的内容(相当于一个非关系型数据库)

ELK技术栈:
elasticsearch结合kibana、Logstash(stash v.贮藏,藏匿)、Beats，也就是elastic stack（ELK）。被广泛应用在日志数据分析、实时监控等领域

elasticsearch底层是基于Lucene来实现的。
Lucene是一个Java语言的搜索引擎类库，是Apache公司的顶级项目，由DougCutting于1999年研发。


### 总结

🔴什么是elasticsearch？

- 一个开源的分布式搜索引擎，可以用来实现搜索、日志统计、分析、系统监控等功能

🔴什么是elastic stack（ELK）？

- 是以elasticsearch为核心的技术栈，包括beats、Logstash、kibana、elasticsearch
logstash,beats负责数据抓取
Kibana负责数据可视化
而elasticsearch是elastic stack的核心，负责存储、搜索、分析数据。

🔴什么是Lucene？

- 是Apache的开源搜索引擎类库，提供了搜索引擎的核心API
 */

/*
## 倒排索引

倒排索引的概念是基于MySQL这样的正向索引而言的。
那么什么是正向索引呢？根据每条数据的id创建索引,然后根据id查询，那么直接走索引，查询速度非常快。
但如果是基于title做模糊查询，只能是逐行扫描数据,随着数据量增加，其查询效率也会越来越低。当数据量达到数百万时，就是一场灾难.

倒排索引中有两个非常重要的概念：
- 文档（`Document`）：用来搜索的数据，原先数据库表中的每一条数据就是一个文档。例如一个网页、一个商品信息
- 词条（`Term`）：对文档数据或用户搜索时的关键词，利用某种算法分词，得到的具备含义的词语就是词条。例如：我是中国人，就可以分为：我、是、中国人、中国、国人这样的几个词条

**创建倒排索引**是对正向索引的一种特殊处理，流程如下：
- 将每一个文档的数据利用算法分词，得到一个个词条
- 创建词条表，每行数据包括词条、词条所在文档id(就是原先数据库表中的id)、位置等信息
- 当出现原先表中已存在的词条时,不再往表中插入新条目,而是通过文档id属性追加,因而词条表中的词条具有唯一性，于是可以给词条创建索引，例如hash表结构索引

      正向索引                             倒排索引
id  title           price           词条(term)       文档id
1   小米手机        3499            小米             1,3,4
2   华为手机        4999            手机             1,2
3   华为小米充电器  49              华为             2,3
4   小米手环        299             充电器           3
                                    手环             4

倒排索引的**搜索流程**如下（以搜索"华为手机"为例）：
1）用户输入条件`"华为手机"`进行搜索。
2）对用户输入内容**分词**，得到词条：`华为`、`手机`。
3）拿着词条在倒排索引中查找，可以得到包含词条的文档id：1、2、3。
4）拿着文档id到正向索引中查找具体文档。
虽然要先查询倒排索引，再查询正向索引，但是无论是词条、还是文档id都建立了索引，查询速度非常快！无需全表扫描。


### 正向和倒排
那么为什么一个叫做正向索引，一个叫做倒排索引呢？
- **正向索引**是最传统的，根据id索引的方式。但根据词条查询时，必须先逐条获取每个文档，然后判断文档中是否包含所需要的词条，是**根据文档找词条的过程**。
- 而**倒排索引**则相反，是先找到用户要搜索的词条，根据词条得到包含词条的文档的id，然后根据id获取文档。是**根据词条找文档的过程**。
是不是恰好反过来了？


那么两者方式的优缺点是什么呢？
**正向索引**：

- 优点：
  - 可以给多个字段创建索引
  - 根据索引字段搜索、排序速度非常快
- 缺点：
  - 根据非索引字段，或者索引字段中的部分词条查找时，只能全表扫描。

**倒排索引**：

- 优点：
  - 根据词条搜索、模糊搜索时，速度非常快
- 缺点：
  - 只能给词条创建索引，而不是字段
  - 无法根据字段做排序
 */

/*
ES与MySQL概念比较

elasticsearch中有很多独有的概念，与mysql中略有差别，但也有相似之处。

elasticsearch是面向**文档（Document）**存储的，可以是数据库中的一条商品数据，一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中

而Json文档中往往包含很多的**字段（Field）**，类似于数据库中的列。

索引和映射
**索引（Index）**，就是相同类型的文档的集合,类似与数据库中的表
例如：
- 所有用户文档，就可以组织在一起，称为用户的索引；
- 所有商品的文档，可以组织在一起，称为商品的索引；
- 所有订单的文档，可以组织在一起，称为订单的索引；

数据库的表会有约束信息，用来定义表的结构、字段的名称、类型等信息。因此，索引库中就有**映射（mapping）**，是索引中文档的字段约束信息，类似表的结构约束。


##我们统一的把mysql与elasticsearch的概念做一下对比：
------------------------------------------------------------------------------
MySQL     Elasticsearch 说明
Table     Index         索引(index)，就是文档的集合，类似数据库的表(table)
Row       Document      文档（Document），就是一条条的数据，类似数据库中的行（Row），文档都是JSON格式
Column    Field         字段（Field），就是JSON文档中的字段，类似数据库中的列（Column）
Schema    Mapping       Mapping（映射）是索引中文档的约束，例如字段类型约束。类似数据库的表结构（Schema）
SQL       DSL           DSL是elasticsearch提供的JSON风格的请求语句，用来操作elasticsearch，实现CRUD,DSL是基于HTTP请求发出去的,elasticsearch对外暴露的是Restful风格接口,与请求发起者的语言无关.DSL=Domain Specific language
------------------------------------------------------------------------------

是不是说，我们学习了elasticsearch就不再需要mysql了呢？

并不是如此，两者各自有自己的擅长支出：
- Mysql：擅长事务类型操作，可以确保数据的安全和一致性
- Elasticsearch：擅长海量数据的搜索、分析、计算

因此在企业中，往往是两者结合使用：
- 对安全性要求较高的写操作，使用mysql实现
- 对查询性能要求较高的搜索需求，使用elasticsearch实现
- 两者再基于某种方式，实现数据的同步，保证一致性

                  搜索操作  →  ElasticSearch
               ↗                     ↑
用户 → 服务器                        ↑
               ↘                   数据同步
                  写操作 → MySQL ↗

 */

/*
分词器


🔴分词器的作用是什么？
- es创建倒排索引时对文档分词
- 用户搜索时，对输入的内容分词


但默认的分词规则对中文处理并不友好。
我们在kibana的DevTools中测试：

POST /_analyze
{
    "analyzer": "standard",
    "text": "黑马程序员学习java太棒了！"
}

语法说明：
POST：请求方式
/_analyze：请求路径，这里省略了http://192.168.150.101:9200，有kibana帮我们补充
请求参数，json风格：
analyzer：分词器类型，这里是默认的standard分词器
text：要分词的内容


处理中文分词，一般会使用IK分词器。https://github.com/medcl/elasticsearch-analysis-ik
🔴IK分词器有2种模式:
- ik_smart：最少切分，粗粒度
- ik_max_word：最细切分，细粒度


ik分词器-拓展&停用词库
要拓展ik分词器的词库 或 禁用某些敏感词条，只需要修改一个ik分词器目录中的config目录中的IkAnalyzer.cfg.xml文件：

<?xml  version="1.0"  encoding="UTF-8"?>
<!DOCTYPE  properties  SYSTEM  "http://java.sun.com/dtd/properties.dtd">
<properties>
    <comment>IK  Analyzer  扩展配置</comment>
    <!--用户可以在这里配置自己的扩展字典-->
    <entry  key="ext_dict">ext.dic</entry>
      <!--用户可以在这里配置自己的扩展停止词字典    ***  添加停用词词典-->
    <entry  key="ext_stopwords">stopword.dic</entry>
</properties>

然后在名为ext.dic的文件中，添加想要拓展的词语 ; 在名为stopword.dic的文件中，添加想要拓展的词语即可


🔴IK分词器如何拓展词条？如何停用词条？
- 利用config目录的IkAnalyzer.cfg.xml文件添加拓展词典和停用词典
- 在词典中添加拓展词条或者停用词条
 */


/*
索引库操作


索引库->数据表
索引库中的文档->表中的每条数据

索引库就类似数据库表，mapping映射就类似表的结构约束(schema)
我们要向es中存储数据，必须先创建"索引库"和"mapping映射属性"


-----------------------------------------------------------------------------

mapping映射属性

mapping是对索引库中文档的约束，常见的mapping属性包括：
- type：字段数据类型，常见的简单类型有：
  - 字符串：text（可分词的文本）、keyword（精确值，例如：品牌、国家、ip地址）
  - 数值：long、integer、short、byte、double、float、
  - 布尔：boolean
  - 日期：date
  - 对象：object
- index：是否创建索引，默认为true
- analyzer：使用哪种分词器,与text配合使用,ik_smart,ik_max_word
- properties：该字段的子字段,object类型数据使用properties声明子字段

例如下面的json文档：
{
    "age":   21,
    "weight":   52.1,
    "isMarried":   false,
    "info":   "黑马程序员Java讲师",
    "email":   "zy@itcast.cn",
    "score":   [99.1, 99.5, 98.9],
    "name":   {
        "firstName":   "云",
        "lastName":   "赵"
    }
}

对应的每个字段映射（mapping）：
- age：类型为 integer；参与搜索，因此需要index为true；无需分词器
- weight：类型为float；参与搜索，因此需要index为true；无需分词器
- isMarried：类型为boolean；参与搜索，因此需要index为true；无需分词器
- info：类型为字符串，需要分词，因此是text；参与搜索，因此需要index为true；分词器可以用ik_smart
- email：类型为字符串，但是不需要分词，因此是keyword；不参与搜索，因此需要index为false；无需分词器
- score：虽然是数组，但是我们只看元素的类型，类型为float；参与搜索，因此需要index为true；无需分词器
- name：类型为object，需要定义多个子属性
  - name.firstName；类型为字符串，但是不需要分词，因此是keyword；参与搜索，因此需要index为true；无需分词器
  - name.lastName；类型为字符串，但是不需要分词，因此是keyword；参与搜索，因此需要index为true；无需分词器


-------------------------------------------------------


创建索引库
来演示
ES中通过Restful请求操作索引库、文档。请求内容用使用Kibana编写DSL语句的方式来表示。创建索引库和mapping的DSL语法如下：

PUT /索引库名称
{
  "mappings": {
    "properties": {
      "字段名":{
        "type": "text",
        "analyzer": "ik_smart"
      },
      "字段名2":{
        "type": "keyword",
        "index": "false"
      },
      "字段名3":{
        "properties": {
          "子字段": {
            "type": "keyword"
          }
        }
      },
      // ...略
    }
  }
}

示例:
#创建索引库
PUT /heima
{
  "mappings": {
    "properties": {
      "info":{
        "type": "text",
        "analyzer": "ik_smart"
      },
      "email": {
        "type": "keyword",
        "index": false
      },
      "name": {
        "type": "object",
        "properties": {
          "firstname": {
            "type": "keyword"
          },
          "lastname": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

-------------------------------------------------------

查看、删除、修改(新增属性)索引库


### 查询索引库
**语法**：
- 请求方式：GET
- 请求路径：/索引库名
- 请求参数：无
**格式**：
GET /索引库名


### 删除索引库
**语法：**
- 请求方式：DELETE
- 请求路径：/索引库名
- 请求参数：无
**格式：**
DELETE /索引库名


### 修改索引库
倒排索引结构虽然不复杂，但是一旦数据结构改变（比如改变了分词器），就需要重新创建倒排索引，这简直是灾难。因此索引库**一旦创建，无法修改mapping**。
虽然无法修改mapping中已有的字段，但是却允许添加新的字段到mapping中，因为不会对倒排索引产生影响。
**格式：**
PUT /索引库名/_mapping
{
  "properties": {
    "新字段名":{
      "type": "integer"
    }
  }
}


# 查看索引库数据结构
GET /索引库名/_mapping


### 总结

索引库操作有哪些？
- 创建索引库：PUT /索引库名
- 查询索引库：GET /索引库名
- 删除索引库：DELETE /索引库名
- 添加字段：PUT /索引库名/_mapping

 */

/*
==================================================================
                <文档操作Create Read Update Delete>
------------------------------------------------------------------


## 总结

文档操作有哪些？

- 创建文档：POST /{索引库名}/_doc/文档id   { json文档 }
- 查询文档：GET /{索引库名}/_doc/文档id
  批量查询文档: GET/{索引库名}/_search
- 删除文档：DELETE /{索引库名}/_doc/文档id
- 修改文档：
  - 全量修改：PUT /{索引库名}/_doc/文档id { json文档 }
  - 增量修改：POST /{索引库名}/_update/文档id { "doc": {某些字段}}


==================================================================
                            新增文档
------------------------------------------------------------------
语法：
POST /索引库名/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    "字段3": {
        "子属性1": "值3",
        "子属性2": "值4"
    },
    // ...
}

示例：
POST /heima/_doc/1
{
    "info": "黑马程序员Java讲师",
    "email": "zy@itcast.cn",
    "name": {
        "firstName": "云",
        "lastName": "赵"
    }
}

==================================================================
                            查询文档
------------------------------------------------------------------
根据rest风格，新增是post，查询应该是get，不过查询一般都需要条件，这里我们把文档id带上。
语法：
GET /{索引库名称}/_doc/{id}

通过kibana查看数据：
GET /heima/_doc/1


# 批量查询索引库中的文档
GET /{索引库名称}/_search

==================================================================
                            删除文档
------------------------------------------------------------------
删除使用DELETE请求，同样，需要根据id进行删除：
语法：
DELETE /{索引库名}/_doc/id值

示例：
DELETE /heima/_doc/1  # 根据id删除数据

==================================================================
                            修改文档
------------------------------------------------------------------
修改有两种方式：
- 全量修改：直接覆盖原来的文档
- 增量修改：修改文档中的部分字段


全量修改是覆盖原来的文档，其本质是：
- 根据指定的id删除文档
- 新增一个相同id的文档
**注意**：如果根据id删除时，id不存在，第二步的新增也会执行，也就从修改变成了新增操作了。

语法：
PUT /{索引库名}/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    // ... 略
}

示例：
#全量修改
PUT /heima/_doc/1
{
  "info":"黑马程序员Java讲师",
  "email":"造纸龙@itcast.cn",
  "name":{
    "firstname":"云",
    "lastname":"赵"
  }
}


增量修改是只修改指定id匹配的文档中的部分字段。
语法：
POST /{索引库名}/_update/文档id
{
    "doc": {
         "字段名": "新的值",
    }
}

示例：
#局部修改文档
POST /heima/_update/1
{
  "doc":{
    "email":"常山造纸龙@qq.com",
    "info":"白马程序员C#讲师"
  }
}


 */


/*
RestHighLevelClient操作索引库

ES官方提供了各种不同语言的客户端，用来操作ES。这些客户端的本质就是组装DSL语句，通过http请求发送给ES。官方文档地址：https://www.elastic.co/guide/en/elasticsearch/client/index.html


ES中支持两种地理坐标数据类型：
geo_point：由纬度（latitude）和经度（longitude）组合成一个字符串确定的一个点。例如："32.8752345, 120.2981576"
geo_shape：有多个geo_point组成的复杂几何图形。例如一条直线，"LINESTRING (-77.03653 38.897676, -77.009051 38.889939)"


字段拷贝可以使用copy_to属性将当前字段拷贝到指定字段。示例：
"all": {
  "type": "text",
  "analyzer": "ik_max_word"
},
"name":{
    "type": "text",
    "analyzer": "ik_max_word",
    "copy_to": "all"
},
"brand": {
  "type": "keyword",
  "copy_to": "all"
},
"business":{
    "type": "keyword",
    "copy_to": "all"
}
可以实现在一个字段里搜到多个字段的内容


#创建酒店的索引库
PUT /hotel
{
  "mappings": {
    "properties": {
      "id": {
        "type": "keyword"
      },
      "name":{
        "type": "text",
        "analyzer": "ik_max_word",
        "copy_to": "all"
      },
      "address":{
        "type": "keyword",
        "index": false
      },
      "price":{
        "type": "integer"
      },
      "score":{
        "type": "integer"
      },
      "brand":{
        "type": "keyword",
        "copy_to": "all"
      },
      "city":{
        "type": "keyword"
      },
      "starName":{
        "type": "keyword"
      },
      "business":{
        "type": "keyword",
        "copy_to": "all"
      },
      "location":{
        "type": "geo_point"
      },
      "pic":{
        "type": "keyword",
        "index": false
      },
      "all":{
        "type": "text",
        "analyzer": "ik_max_word"
      }
    }
  }
}

------------------------------------------------------

### 初始化RestHighLevelClient

在elasticsearch提供的API中，与elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中，必须先完成这个对象的初始化，建立与elasticsearch的连接。

分为三步：

1）引入es的RestHighLevelClient依赖：
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>

2）因为SpringBoot默认的ES版本是7.6.2，所以我们需要覆盖默认的ES版本：
<properties>
    <java.version>1.8</java.version>
    <!--SpringBoot项目需要再properties中指定elasticsearch版本,与服务端一致-->
    <!--因为SpringBoot默认的ES版本是7.6.2，所以我们需要覆盖默认的ES版本-->
    <elasticsearch.version>7.12.1</elasticsearch.version>
</properties>

3）初始化RestHighLevelClient：
初始化的代码如下：
private RestHighLevelClient restHighLevelClient;
...
restHighLevelClient = new RestHighLevelClient(
    RestClient.builder(
        HttpHost.create("http://192.168.150.150:9200")
    )
);//初始化RestHighLevelClient
...
restHighLevelClient.close();//销毁客户端

-------------------------------------------------------------------

创建索引库


 //1.创建createIndexRequest对象
CreateIndexRequest createIndexRequest = new CreateIndexRequest("hotel");//索引库名称,对应DSL /hotel
//2.准备请求的参数,DSL的JSON参数部分对应{"mappings":{...}}部分
createIndexRequest.source(MAPPING_TEMPLATE, XContentType.JSON);
//3.发送请求   indices()返回的对象中包含索引库操作的所有方法
restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT);

代码分为三步：

- 1）创建Request对象。因为是创建索引库的操作，因此Request是CreateIndexRequest。
- 2）添加请求参数，其实就是DSL的JSON参数部分。因为json字符串很长，这里是定义了静态字符串常量MAPPING_TEMPLATE(往上翻,有)，让代码看起来更加优雅。
- 3）发送请求，restHighLevelClient.indices()方法的返回值是IndicesClient类型，封装了所有与索引库操作有关的方法。

-------------------------------------------------------------------

删除索引库

//1.创建Request对象
DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("hotel");//DELETE /heima
//删除索引库无需JSON参数
//2.发送请求
restHighLevelClient.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT);

-------------------------------------------------------------------

查询索引库是否存在

//1.创建Request对象
GetIndexRequest getIndexRequest = new GetIndexRequest("hotel");//GET /hotel
//查询索引库无需JSON参数
//2.发送请求
boolean exists = restHighLevelClient.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
//3.输出
System.err.println(exists ? "索引库已存在!" : "索引库不存在!");

-------------------------------------------------------------------

## 总结

JavaRestClient操作elasticsearch的流程基本类似。核心是restHighLevelClient.indices()方法来获取索引库的操作对象。

索引库操作的基本步骤：

- 初始化RestHighLevelClient
- 创建XxxIndexRequest。XXX是Create、Get、Delete
- 准备JSON字符串（ Create时需要，其它是无参）
- 发送请求。调用restHighLevelClient.indices().xxx()方法，xxx是create、exists、delete

 */


/*
RestHighLevelClient操作文档

新增文档

@Test
void testAddDocument() throws IOException {
    //根据id查询酒店数据
    Hotel hotel = iHotelService.getById(61083L);
    //数据库行类型 转换为 索引库文档类型
    HotelDoc hotelDoc = new HotelDoc(hotel);

    //1.准备Request对象
    IndexRequest indexRequest = new IndexRequest("hotel").id(hotelDoc.getId().toString());//索引库中文档id同数据库各行
//2.准备请求参数JSON字符串
    String DSLJsonStr = JSON.toJSONString(hotelDoc);//将hotelDoc序列化成josn字符串
    System.err.println("DSLJsonStr=" + DSLJsonStr);
    indexRequest.source(DSLJsonStr, XContentType.JSON);
//3.发送请求
    restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
}

-----------------------------------------------------------------

查询文档

@Test
void testGetDocumentById() throws IOException {
    //1.准备Request
    GetRequest getRequest = new GetRequest("hotel", "61083");
    //2.发送请求,得到响应
    GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
    //3.解析响应结果jsonStr
    String jsonStr = getResponse.getSourceAsString();
    HotelDoc hotelDoc = JSON.parseObject(jsonStr, HotelDoc.class);
    System.err.println(hotelDoc);

    //HotelDoc(id=61083, name=上海滴水湖皇冠假日酒店, address=自由贸易试验区临港新片区南岛1号, price=971, score=44, brand=皇冠假日, city=上海, starName=五钻, business=滴水湖临港地区, location=30.890867, 121.937241, pic=https://m.tuniucdn.com/fb3/s1/2n9c/312e971Rnj9qFyR3pPv4bTtpj1hX_w200_h200_c1_t0.jpg)
}

-----------------------------------------------------------------

修改文档

修改我们讲过两种方式：
- 全量修改：本质是先根据id删除，再新增
- 增量修改：修改文档中的指定字段值

在RestClient的API中，全量修改与新增的API完全一致，判断依据是ID：
- 如果新增时，ID已经存在，则全量修改(先删除,再新增)
- 如果新增时，ID不存在，则新增
这里不再赘述，我们主要关注增量修改。

@Test
void testUpdateDocumentById() throws IOException {
    //1.准备Request
    UpdateRequest updateRequest = new UpdateRequest("hotel", "61083");
    //2.准备请求参数
    updateRequest.doc("price", 952, "starName", "四钻");
    //3.发送请求
    restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
}

-----------------------------------------------------------------

删除文档

@Test
void testDeleteDocumentById() throws IOException {
    //1.准备Request
    DeleteRequest deleteRequest = new DeleteRequest("hotel", "61083");
    //2.发送请求
    restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
}

-----------------------------------------------------------------

批量导入文档


步骤如下：
- 利用mybatis-plus查询酒店数据
- 将查询到的酒店数据（Hotel）转换为文档类型数据（HotelDoc）
- 利用JavaRestClient中的BulkRequest批处理，实现批量新增文档

批量处理BulkRequest，其本质就是将多个普通的CRUD请求组合在一起发送。
其中提供了一个add方法，用来添加其他请求,能添加的请求包括：
- IndexRequest，也就是新增
- UpdateRequest，也就是修改
- DeleteRequest，也就是删除
因此Bulk中添加了多个IndexRequest，就是批量新增功能了。

@Test
void testBulkDocument() throws IOException {
    //MyBatisPlus从数据库批量查询酒店数据
    List<Hotel> hotels = iHotelService.list();


    //1.创建BulkRequest
    BulkRequest bulkRequest = new BulkRequest();
    //2.准备参数,添加多个新增文档的IndexRequest对象

    //for循环遍历List<Hotel>每转换一个HotelDoc就添加一个IndexRequest
    for (Hotel hotel : hotels) {
        //将数据库行类型Hotel转换为索引库文档类型HotelDoc
        HotelDoc hotelDoc = new HotelDoc(hotel);
        //添加新增文档的IndexRequest对象
        bulkRequest.add(new IndexRequest("hotel").id(hotelDoc.getId().toString()).source(JSON.toJSONString(hotelDoc), XContentType.JSON));
    }

    //3.发送请求
    restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
}

------------------------------------------------------------------

## 小结

文档操作的基本步骤：

- 初始化RestHighLevelClient
- 创建XxxRequest。XXX是Index、Get、Update、Delete、Bulk
- 准备参数（Index、Update、Bulk时需要）
- 发送请求。调用restHighLevelClient.xxx()方法，xxx是index、get、update、delete、bulk
- 解析结果（Get时需要）

 */



/*
DSL查询语法


## DSL查询分类

Elasticsearch提供了基于JSON的DSL[Domain Specific Language]特殊领域语言 来定义查询。

常见的查询类型包括：

- 查询所有：查询出所有数据，一般测试用。例如：match_all
- 全文检索（full text）查询：利用分词器对用户输入内容分词，然后去倒排索引库中匹配。例如：
  - match_query
  - multi_match_query
- 精确查询：根据精确词条值查找数据，一般是查找keyword、数值、日期、boolean等类型字段。例如：
  - ids
  - range
  - term
- 地理（geo）查询：根据经纬度查询。例如：
  - geo_distance
  - geo_bounding_box
- 复合（compound）查询：复合查询可以将上述各种查询条件组合起来，合并查询条件。例如：
  - bool
  - function_score



-------------------------------------------------------------
## 查询所有

查询的语法基本一致：
GET /indexName/_search
{
  "query": {
    "查询类型": {
      "查询条件": "条件值"
    }
  }
}

我们以查询所有为例，其中：

- 查询类型为match_all
- 没有查询条件

// 查询所有
GET /indexName/_search
{
  "query": {
    "match_all": {
    }
  }
}

其它查询无非就是**查询类型**、**查询条件**的变化。



-------------------------------------------------------------
## 全文检索查询


### 使用场景

全文检索查询的基本流程如下：
- 对用户搜索的内容做分词，得到词条
- 根据词条去倒排索引库中匹配，得到文档id
- 根据文档id找到文档，返回给用户

比较常用的场景包括：
- 商城的输入框搜索
- 百度输入框搜索

因为是拿着词条去匹配，因此参与搜索的字段也必须是可分词的text类型的字段。



### 基本语法
常见的全文检索查询包括：
- match查询：会对用户输入内容分词，然后去倒排索引库按照单字段查询

- multi_match查询：多字段查询，任意一个字段符合条件就算符合查询条件


全文检索查询-match单字段查询语法如下：
GET /indexName/_search       GET /hotel/_search
{
  "query":{
    "match": {
        "FIELD":"TEXT"     "all": "外滩如家"
    }
  }
}

全文检索查询-multi_match多字段查询语法如下：
GET /indexName/_search        GET /hotel/_search
{
  "query":{
    "multi_match": {
      "query": "TEXT",             "query": "外滩如家"
      "fields": ["FIELD1","FIELD12"]         "fields": ["brand","name","business"]
    }
  }
}

🔴可以看到，两种查询结果是一样的，为什么？
因为我们在创建索引库定义mapping映射属性时,通过使用copy_to属性将多个字段(brand、name、business)拷贝到指定字段(all)。因此根据三个字段(brand、name、business)搜索，和根据all字段搜索效果当然一样了。
但是，搜索字段越多，对查询性能影响越大，因此建议采用copy_to属性拷贝多个字段到一个字段，然后以单字段查询的方式实现多字段查询。


### 总结

match和multi_match的区别是什么？

- match：根据一个字段查询(但是可以在创建索引库定义mapping映射属性时,通过使用copy_to属性将多个字段拷贝到指定字段,实现以单字段查询多字段的效果)
- multi_match：根据多个字段查询，参与查询字段越多，查询性能越差



-------------------------------------------------------------
## 精准查询

精确查询一般是查找keyword、数值、日期、boolean等类型字段,这些值是不可分割的整体,所以不会对搜索条件分词。常见的精确查询类型有：
- term：根据词条精确值查询
- range：根据值的范围查询



### term查询

因为精确查询的字段搜是不分词的字段，因此查询的条件也必须是**不分词**的词条。查询时，用户输入的内容跟字段值完全匹配时才认为符合条件。如果用户输入的内容过多，反而搜索不到数据。

语法说明：

# term精确查询
GET /indexName/_search      GET /hotel/_search
{
  "query": {
    "term": {
      "FIELD": {                "brand": {
        "value": "VALUE"          "value": "7天酒店"
      }
    }
  }
}


### range查询

范围查询，一般应用在对数值类型做范围过滤的时候。比如做价格范围过滤。

语法说明：

// range查询
GET /indexName/_search           GET /hotel/_search
{
  "query": {
    "range": {
      "FIELD": {
        "gte": 10, // 这里的gte代表大于等于greater than or equal，gt则代表大于greater than
        "lte": 20 // lte代表小于等于less than or equal，lt则代表小于less than
      }
    }
  }
}


### 总结

精确查询常见的有哪些？

- term查询：根据词条精确匹配，一般搜索keyword类型、数值类型、布尔类型、日期类型字段
- range查询：根据数值范围查询，可以是数值、日期的范围


--------------------------------------------------------------------
地理坐标查询

所谓的地理坐标查询，其实就是根据经纬度查询，官方文档：https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html

常见的使用场景包括：

- 携程：搜索我附近的酒店
- 滴滴：搜索我附近的出租车
- 微信：搜索我附近的人


### 矩形范围查询
矩形范围查询，也就是geo_bounding_box查询，查询坐标落在某个矩形范围的所有文档：
查询时，需要指定矩形的**左上**、**右下**两个点的坐标，然后画出一个矩形，落在该矩形内的都是符合条件的点。

语法如下：
// geo_bounding_box查询
GET /indexName/_search
{
  "query": {
    "geo_bounding_box": {
      "FIELD": {
        "top_left": { // 左上点
          "lat": 31.1,
          "lon": 121.5
        },
        "bottom_right": { // 右下点
          "lat": 30.9,
          "lon": 121.7
        }
      }
    }
  }
}

### 附近查询
附近查询，也叫做距离查询（geo_distance）：查询到指定中心点小于某个距离值的所有文档。
换句话来说，在地图上找一个点作为圆心，以指定距离为半径，画一个圆，落在圆内的坐标都算符合条件：

语法说明：
// geo_distance 查询
GET /indexName/_search
{
  "query": {
    "geo_distance": {
      "distance": "15km", // 半径
      "FIELD": "31.21,121.5" // :后面是圆心纬度经度  FIELD字段为geo_point类型
    }
  }
}

---------------------------------------------------------
复合查询

复合（compound）查询：复合查询可以将其它简单查询组合起来，实现更复杂的搜索逻辑。常见的有两种：

- fuction score：算分函数查询，可以控制文档相关性算分，控制文档排名,如百度竞价
- bool query：布尔查询，利用逻辑关系组合多个其它的查询，实现复杂搜索



### 相关性算分
当我们利用match查询时，文档结果会根据与搜索词条的关联度打分（_score），返回结果时按照分值降序排列。


#总结
elasticsearch中的相关性打分算法是什么？

在elasticsearch5.0之前:TF-IDF. 其特点是分数会随着词频增加而越来越大
在elasticsearch5.0之后:BM25.   其特点是分数会随着词频增加而增大，但增长曲线会趋于水平

TF-IDF算法有缺陷，就是词条频率越高，文档得分也会越高，单个词条对文档影响较大。
而BM25则会让单个词条的算分有一个上限，曲线更加平滑



🔴算分函数查询function score query
根据相关度打分是比较合理的需求，但**合理的不一定是产品经理需要**的。
以百度为例，你搜索的结果中，并不是相关度越高排名越靠前，而是谁掏的钱多排名就越靠前。
要想人为控制相关性算分，就需要利用elasticsearch中的function score 查询了

使用 function score query，可以修改文档的相关性算分（query score），根据新得到的算分排序。

语法说明:
GET /hotel/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "all": "外滩"# 原始查询条件，搜索文档并根据相关性打分(query score)
        }
      },
      "functions": [
        {
          "filter":{
            "term": {
              "brand": "如家"# 过滤条件，符合条件的文档才会被重新算分
            }
          },
          "weight": 10
          # 算分函数，算分函数的结果称为function score ，将来会与query score运算，得到新算分，常见的算分函数有：
            weight：给一个常量值，作为函数结果（function score）
            field_value_factor：用文档中的某个字段值作为函数结果
            random_score：随机生成一个值，作为函数结果
            script_score：自定义计算公式，公式结果作为函数结果
        }
      ],
      "boost_mode": "sum"
      # 加权模式，定义function score与query score的运算方式，包括：
        multiply：两者相乘。默认就是这个
        replace：用function score 替换 query score
        其它：sum求和、avg平均值、max最大者、min最小者
    }
  }
}


function score 查询中包含四部分内容：

- **原始查询**条件：query部分，基于这个条件搜索文档，并且基于BM25算法给文档打分，**原始算分**（query score)
- **过滤条件**：filter部分，符合该条件的文档才会重新算分
- **算分函数**：符合filter条件的文档要根据这个函数做运算，得到的**函数算分**（function score），有四种函数
  - weight：函数结果是常量
  - field_value_factor：以文档中的某个字段值作为函数结果
  - random_score：以随机数作为函数结果
  - script_score：自定义算分函数算法
- **运算模式**：算分函数的结果、原始查询的相关性算分，两者之间的运算方式，包括：
  - multiply：相乘
  - replace：用function score替换query score
  - 其它，例如：sum、avg、max、min


  function score的运行流程如下：

- 1）根据**原始条件**查询搜索文档，并且计算相关性算分，称为**原始算分**（query score）
- 2）根据**过滤条件**，过滤文档
- 3）符合**过滤条件**的文档，基于**算分函数**运算，得到**函数算分**（function score）
- 4）将**原始算分**（query score）和**函数算分**（function score）基于**运算模式**做运算，得到最终结果，作为相关性算分。


因此，其中的关键点是：

- 过滤条件：决定哪些文档的算分被修改
- 算分函数：决定函数算分的算法
- 运算模式：决定最终算分结果



#### 小结
function score query算分函数查询定义的三要素是什么？
- 过滤条件：哪些文档要加分
- 算分函数：如何计算function score
- 加权方式：function score 与 query score如何运算



🔴布尔查询Boolean Query
布尔查询是一个或多个查询子句的组合，每一个子句就是一个**子查询**。子查询的逻辑关系有：

- must：必须匹配每个子查询，类似“与”
- should：选择性匹配子查询，类似“或”
- must_not：必须不匹配，**不参与算分**，类似“非”
- filter：必须匹配，**不参与算分**

比如在搜索酒店时，除了关键字搜索外，我们还可能根据品牌、价格、城市等字段做过滤
每一个不同的字段，其查询的条件、方式都不一样，是多个不同的查询，而要组合这些查询，就必须用bool查询了

需要注意的是，搜索时，参与**打分的字段越多，查询的性能也越差**。因此这种多条件查询时，建议这样做：
- 搜索框的关键字搜索，是全文检索查询，使用must查询，参与算分
- 其它过滤条件，采用filter查询。不参与算分

语法示例：

GET /hotel/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "name": "如家"
          }
        }
      ],
      "should": [
        {
          "term": {
            "id": "200215365"
          }
        },
         {
          "term": {
            "id": "433576"
          }
        }
      ],
      "must_not": [
        {
          "range": {
            "price": {
              "gt": 400
            }
          }
        }
      ],
      "filter": [
        {
          "geo_distance": {
            "distance": "15km",
            "location": {
              "lat": 31.23,
              "lon": 121.48
            }
          }
        }
      ]
    }
  }
}
 */


/*
搜索结果处理-排序

elasticsearch默认是根据相关度算分（_score）来排序，但是也支持自定义方式对搜索结果排序。可以排序字段类型有：keyword类型、数值类型、地理坐标类型、日期类型等。

-------------------------------------------------------

### 普通字段排序
keyword、数值、日期类型排序的语法基本一致。

语法：
GET /indexName/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "score":"desc"  // 排序字段、排序方式ASC、DESC
    },
    {
      "price":"asc"
    }
  ]
}

排序条件是一个数组，也就是可以写多个排序条件。按照声明的顺序，当第一个条件相等时，再按照第二个条件排序，以此类推

-------------------------------------------------------

### 地理坐标排序
地理坐标排序略有不同。

语法：
GET /indexName/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "_geo_distance" : {
          "FIELD" : "纬度，经度", // 文档中geo_point类型的字段名、目标坐标点(圆心)经纬度

          也可以这种格式:
          "FIELD": {
          "lon": 经度,
          "lat": 纬度
          },

          "order" : "asc", // 排序方式 asc/desc
          "unit" : "km" // 排序的距离单位
      }
    }
  ]
}

这个查询的含义是：

- 指定一个坐标，作为目标点(圆心)
- 计算每一个文档中指定字段（必须是geo_point类型）的坐标,到目标点(圆心)的距离是多少
- 根据距离排序

按照地理坐标排序后将不再产生**原始算分**（query score）,不再按照原始算分排序

*/


/*
搜索结果处理-分页


elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。elasticsearch中通过修改from、size参数来控制要返回的分页结果：

- from：起始索引 (pageNum-1)*pageSize
- size：页的大小  pageSize

类似于mysql中的
select 字段列表 from    表名   limit       起始索引       , 查询记录数
select    *     from tableName limit (pageNum-1)*pageSize , pageSize

-------------------------------------------------------

### 基本的分页
分页的基本语法如下：

GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0, // 分页开始的位置，默认为0
  "size": 10, // 期望获取的文档总数
  "sort": [
    {"price": "asc"}
  ]
}

-------------------------------------------------------

### 深度分页问题

现在，我要查询990~1000的数据，查询逻辑要这么写：
GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "from": 990, // 分页开始的位置，默认为0
  "size": 10, // 期望获取的文档总数
  "sort": [
    {"price": "asc"}
  ]
}
这里是查询990开始的数据，也就是 第990~第1000条 数据。

不过，elasticsearch内部因为是倒排索引实现的,所以分页时,必须先查询 0~1000条，然后截取其中的990 ~ 1000的这10条

查询TOP1000，如果es是单点模式，这并无太大影响。

但是elasticsearch将来一定是集群，例如我集群有5个节点，我要查询TOP1000的数据，并不是每个节点查询200条就可以了。

因为节点A的TOP200，在另一个节点可能排到10000名以外了。

因此要想获取整个集群的TOP1000，必须先查询出每个节点的TOP1000，汇总结果后，重新排名，重新截取TOP1000。

那如果我要查询9900~10000的数据呢？是不是要先查询TOP10000呢？那每个节点都要查询10000条？汇总到内存中？

当查询分页深度较大时，汇总数据过多，对内存和CPU会产生非常大的压力，因此elasticsearch会禁止from+ size 超过10000的请求。

针对深度分页，ES提供了两种解决方案，[官方文档](https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html)：

- search after：分页时需要排序，原理是从上一次的排序值开始，查询下一页数据。官方推荐使用的方式,问题是只能往后翻页不能往前.
- scroll：原理将排序后的文档id形成快照，保存在内存,占用内存较大。官方已经不推荐使用。

-------------------------------------------------------

### 小结

分页查询的常见实现方案以及优缺点：

- `from + size`：
  - 优点：支持随机翻页
  - 缺点：深度分页问题，默认查询上限（from + size）是10000
  - 场景：百度、京东、谷歌、淘宝这样的随机翻页搜索
- `after search`：
  - 优点：没有查询上限（单次查询的size不超过10000）
  - 缺点：只能向后逐页查询，不支持随机翻页
  - 场景：没有随机翻页需求的搜索，例如手机向下滚动翻页
- `scroll`：
  - 优点：没有查询上限（单次查询的size不超过10000）
  - 缺点：会有额外内存消耗，并且搜索结果是非实时的
  - 场景：海量数据的获取和迁移。从ES7.1开始不推荐，建议用 after search方案。


 */


/*
搜索结果处理-高亮

什么是高亮显示呢？
我们在百度，京东搜索时，关键字会变成红色，比较醒目，这叫高亮显示：

高亮显示的实现分为两步：
- 1）给文档中的所有关键字都添加一个标签，例如`<em>`标签
- 2）页面给`<em>`标签编写CSS样式


### 实现关键字高亮

高亮的语法：
GET /hotel/_search
{
  "query": {
    "match": {
      "FIELD": "TEXT" // 查询条件，高亮一定要使用全文检索查询
    }
  },
  "highlight": {
    "fields": { // 指定要高亮的字段
      "FIELD": {
        "pre_tags": "<em>",  // 用来标记高亮字段的前置标签,不写默认也是添加<em>前缀
        "post_tags": "</em>" // 用来标记高亮字段的后置标签,不写默认也是添加</em>后缀
      }
    }
  }
}

**注意：**
- 高亮是对关键字高亮，因此**搜索条件必须带有关键字**，而不能是范围查询。
- 默认情况下es搜索字段必须与高亮字段一致,否则不会添加高亮标记
  通过给高亮字段添加"require_field_match": "false"属性,可以让高亮字段与搜索字段不匹配也能高亮

  通常搜索字段是all,all字段中是多个字段copy_to拷贝过来的,可以实现以单字段查询多字段的效果,而需要添加高亮的字段只有其中一个或几个,此时需要添加"require_field_match": "false"才能高亮
示例:
GET /hotel/_search
{
    "query": {
      "match": {
        "all": "如家"
      }
    },
    "highlight": {
      "fields": {
        "name":{
           "pre_tags": "<em>"
           ,"post_tags": "</em>"
           ,"require_field_match": "false"
        }
      }
    }
}

*/


/*
## 查询结果处理总结

查询的DSL是一个大的JSON对象，包含下列属性：

- query：查询条件
- from和size：分页条件
- sort：排序条件
- highlight：高亮条件


# 搜索结果各种处理完整演示
GET /hotel/_search
{
  "query": {
    "match": {
      "name": "喜来登"//全文检索查询-match单字段查询
    }
  },

  "from": 1, //分页开始索引,第一页是0
  "size": 2, //页面大小

  "sort": [
    {
      "price":"asc"//普通字段排序
    },
    {
      "_geo_distance": {//地理坐标排序
        "location": {
          "lat": 31.040699,
          "lon": 121.618075
        },
        "order": "desc",
        "unit": "km"
      }
    }
  ],
  "highlight": {//关键字高亮
    "fields": {
      "name": {
        "pre_tags": "<em>",
        "post_tags": "</em>"
      }
    }
  }
}
 */



/*
使用 RestHighLevelClient 实现各种复杂查询 & 查询结果集处理


------------------------------------------------------------------------
查询所有match_all查询

@Test
    void testMatchAll() throws IOException {
        //1.准备Request
        SearchRequest searchRequest = new SearchRequest("hotel");
        //2.准备DSL
        searchRequest.source().query(QueryBuilders.matchAllQuery());
        //3.发送请求
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
//        System.err.println(searchResponse);//得到的结果SearchResponse就是DSL查询响应的json字符串



         "hits" : {
            "total" : {
              "value" : 11,
              "relation" : "eq"
            },
            "max_score" : null,
            "hits" :[
                {
                    "_index" : "hotel",
                    "_type" : "_doc",
                    "_id" : "396506",
                    "_score" : null,
                    "_source" : {
                      "address" : "北三环东路36号",
                      "brand" : "喜来登",
                      "business" : "马甸/安贞地区",
                      "city" : "北京",
                      "id" : 396506,
                      "location" : "39.967163, 116.4099",
                      "name" : "北京金隅喜来登酒店",
                      "pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/29FW2WtGzzUtPhWR1LKxcFZAVa9P_w200_h200_c1_t0.jpg",
                      "price" : 357,
                      "score" : 47,
                      "starName" : "五星级"
                    }
                },
                ...
            ]
          }



    search返回的结果是一个JSON字符串，结构包含：

    - `hits`：命中的结果
    - `total`：总条数，其中的value是具体的总条数值
    - `max_score`：所有结果中得分最高的文档的相关性算分
    - `hits`：搜索结果的文档数组，其中的每个文档都是一个json对象
    - `_source`：文档中的原始数据，也是json对象

    因此，我们解析响应结果，就是逐层解析JSON字符串，流程如下：

    - `SearchHits`：通过response.getHits()获取，就是JSON中的最外层的hits，代表命中的结果
    - `SearchHits#getTotalHits().value`：获取总条数信息
    - `SearchHits#getHits()`：获取SearchHit数组，也就是文档数组
    - `SearchHit#getSourceAsString()`：获取文档结果中的_source，也就是原始的json文档数据


    //4.解析响应json字符串
    SearchHits searchHits = searchResponse.getHits();//包含total,max_score,hits数组
    //4.1获取总条数
    long total = searchHits.getTotalHits().value;//搜索到的总数
        System.err.println("共搜索到" + total + "条数据");
    //4.2获取文档数组
    SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
        //4.3遍历获取hits数组中每文档里_source属性的值,是个json字符串
        String jsonStr = hit.getSourceAsString();
        //4.4反序列化jsonStr转成实体类对象
        HotelDoc hotelDoc = JSON.parseObject(jsonStr, HotelDoc.class);
        System.err.println("hotelDoc:" + hotelDoc);
    }
}

### 小结

查询的基本步骤是：

1. 创建SearchRequest对象

2. 准备Request.source()，也就是DSL。

   ① QueryBuilders来构建查询条件

   ② 传入Request.source() 的 query() 方法

3. 发送请求，得到结果

4. 解析结果（参考JSON结果，从外到内，逐层解析）


---------------------------------------------------------------------
全文检索查询 单字段match 多字段multi_match

全文检索的match和multi_match查询与match_all的API基本一致。差别是查询条件，也就是query的部分。

因此，Java代码上的差异主要是request.source().query()中的参数了。同样是利用QueryBuilders提供的方法:

//单字段查询
QueryBuilders.matchQuery("all","如家");
//多字段查询
QueryBuilders.multiMatchQuery("如家","name","business");

而结果解析代码则完全一致，可以抽取并共享。

完整代码如下：

@Test
void testMatch() throws IOException {
    //1.准备Request
    SearchRequest searchRequest = new SearchRequest("hotel");
    //2.准备DSL
    searchRequest.source().query(QueryBuilders.matchQuery("all","如家"));
    //3.发送请求
    SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
    //        System.err.println(searchResponse);//得到的结果SearchResponse就是DSL查询响应的json字符串
    handleResponse(searchResponse);
}

-----------------------------------------------------------------
精确查询

- term：词条精确匹配
- range：范围查询

与之前的查询相比，差异同样在查询条件，其它都一样。

查询条件构造的API如下：

//词条查询
QueryBuilders.termQuery("city","杭州");
//范围查询
QueryBuilders.rangeQuery("price").gte(100).lte(150);

--------------------------------------------------------------------
布尔查询

用must、must_not、filter等方式组合其它查询，代码示例如下：

@Test
void testBool() throws IOException {

    //1.准备Request
    SearchRequest searchRequest = new SearchRequest("hotel");

    //2.准备DSL
    //2.1准备BoolQueryBuilder
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    //2.2 must中添加term精确查询
    boolQuery.must(QueryBuilders.termQuery("city","上海"));
    //2.3 filter中添加range精确查询
    boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));
    //2.4 将QueryBuilder传递给searchRequest对象
    searchRequest.source().query(boolQuery);

    //3.发送请求
    SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
    //        System.err.println(searchResponse);//得到的结果SearchResponse就是DSL查询响应的json字符串
    handleResponse(searchResponse);
}

可以看到，API与其它查询的差别同样是在查询条件的构建，QueryBuilders，结果解析等其他代码完全不变。

-------------------------------------------------------------------
## 算分函数查询 function score query

Function Score查询可以控制文档的相关性算分，使用方式如下：

GET /hotel/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "all": "外滩"# 原始查询条件，搜索文档并根据相关性打分(query score)
        }
      },
      "functions": [
        {
          "filter":{
            "term": {
              "brand": "如家"# 过滤条件，符合条件的文档才会被重新算分
            }
          },
          "weight": 10
          # 算分函数，算分函数的结果称为function score ，将来会与query score运算，得到新算分，常见的算分函数有：
            weight：给一个常量值，作为函数结果（function score）
            field_value_factor：用文档中的某个字段值作为函数结果
            random_score：随机生成一个值，作为函数结果
            script_score：自定义计算公式，公式结果作为函数结果
        }
      ],
      "boost_mode": "sum"
      # 加权模式，定义function score与query score的运算方式，包括：
        multiply：两者相乘。默认就是这个
        replace：用function score 替换 query score
        其它：sum求和、avg平均值、max最大者、min最小者
    }
  }
}


FunctionScoreQueryBuilder functionScoreQueryBuilder =
    QueryBuilders.functionScoreQuery(
        QueryBuilders.matchQuery("name", "外滩"),
        new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
            new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                QueryBuilders.termQuery("brand", "如家"),
                ScoreFunctionBuilders.weightFactorFunction(5)
            )
        }
    );
sourceBuilder.query(functionScoreQueryBuilder);







---------------------------------------------------------------
## 排序、分页

搜索结果的排序和分页是与query同级的参数，因此同样是使用request.source()来设置。


# 普通字段排序

GET /hotel/_search
{
  "query": {
    "match": {
      "name": "喜来登"//全文检索查询-match单字段查询
    }
  },
  "from": 1, //分页开始索引,第一页是0
  "size": 2, //页面大小
  "sort": [
    {
      "price":"asc"//普通字段排序
    }
  ]
}


@Test
void testPagingAndSort() throws IOException {

    //1.准备Request
    SearchRequest searchRequest = new SearchRequest("hotel");

    //2.1 query
    searchRequest.source().query(QueryBuilders.matchAllQuery());
    //2.2 sort
    searchRequest.source().sort("price", SortOrder.ASC);
    //2.3 paging
    //前端传过来的页码,每页大小
    int pageNumber = 2 , pageSize = 5;
    searchRequest.source().from((pageNumber - 1) * pageSize).size(pageSize);

    //3.发送请求
    SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
    //        System.err.println(searchResponse);//得到的结果SearchResponse就是DSL查询响应的json字符串
            handleResponse(searchResponse);
}


# 距离排序
距离排序与普通字段排序有所差异，API如下：

GET /indexName/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "price": "asc"
    },
    {
      "_geo_distance" : {
          "FIELD" : "纬度，经度",
          "order" : "asc",
          "unit" : "km"
      }
    }
  ]
}

// 价格排序
request.source().sort("price", SortOrder.ASC);
// 距离排序
request.source().sort(SortBuilders
    .geoDistanceSort("location", new GeoPoint("31.21, 121.5"))
    .order(SortOrder.ASC)
    .unit(DistanceUnit.KILOMETERS)
);



-------------------------------------------------------------------
## 高亮

高亮的代码与之前代码差异较大，有两点：

- 查询的DSL：其中除了查询条件，还需要添加高亮条件，同样是与query同级。
- 结果解析：结果除了要解析_source文档数据，还要解析高亮结果


GET /hotel/_search
{
    "query": {
      "match": {
        "all": "如家"
      }
    },
    "highlight": {
      "fields": {
        "name":{
           "pre_tags": "<em>",
           "post_tags": "</em>"
           , "require_field_match": "false"
        }
      }
    }
}


@Test
void testHighLight() throws IOException {

    //1.准备Request
    SearchRequest searchRequest = new SearchRequest("hotel");

    //2.1 match query
    searchRequest.source().query(QueryBuilders.matchQuery("all","如家"));
    //2.2 highlight
    searchRequest.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false).preTags("<em>").postTags("</em>"));

    //3.发送请求
    SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
    //        System.err.println(searchResponse);//得到的结果SearchResponse就是DSL查询响应的json字符串
    handleResponseForHighlight(searchResponse);
}



### 高亮结果解析

高亮的结果与查询的文档结果默认是分离的，并不在一起。
因此解析高亮的代码需要额外处理：


DSL高亮结果结构:
  {
    "_index" : "hotel",
    "_type" : "_doc",
    "_id" : "339952837",
    "_score" : 2.756186,
    "_source" : {
      "address" : "良乡西路7号",
      "brand" : "如家",
      "business" : "房山风景区",
      "city" : "北京",
      "id" : 339952837,
      "location" : "39.73167, 116.132482",
      "name" : "如家酒店(北京良乡西路店)",
      "pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/3Dpgf5RTTzrxpeN5y3RLnRVtxMEA_w200_h200_c1_t0.jpg",
      "price" : 159,
      "score" : 46,
      "starName" : "二钻"
    },
    "highlight" : {
      "name" : [
        "<em>如家</em>酒店(北京良乡西路店)"
      ]
    }
  }


private void handleResponseForHighlight(SearchResponse searchResponse) {
    //4.解析响应json字符串
    SearchHits searchHits = searchResponse.getHits();//包含total,max_score,hits数组
    //4.1获取总条数
    long total = searchHits.getTotalHits().value;//搜索到的总数
    System.err.println("共搜索到" + total + "条数据");
    //4.2获取文档数组
    SearchHit[] hits = searchHits.getHits();
    for (SearchHit hit : hits) {
        //4.3遍历获取hits数组中每文档里_source属性的值,是个json字符串
        String jsonStr = hit.getSourceAsString();
        //4.4反序列化jsonStr转成实体类对象
        HotelDoc hotelDoc = JSON.parseObject(jsonStr, HotelDoc.class);

        //🔴用highlight中字段的值替换原来字段的值
        //"highlight" : {
        //      "name" : [
        //        "<em>如家</em>酒店(北京良乡西路店)"
        //      ]
        //    }
        //获取高亮结果
        Map<String, HighlightField> highlightFields = hit.getHighlightFields();
        if (highlightFields != null && highlightFields.size() != 0) {//当Map集合不为空,且长度不为0时
            *//*可以使用集合工具类判空,效果同上
            if(CollectionUtils.isEmpty(highlightFields))*//*
            //根据字段名获取高亮结果
            HighlightField highlightField = highlightFields.get("name");
            if (highlightField != null) {//当存在键值对name时
                //获取数组中的高亮值
                String nameWithHighlight = highlightField.getFragments()[0].string();
                //覆盖高亮结果
                hotelDoc.setName(nameWithHighlight);
                System.err.println("hotelDoc:" + hotelDoc);
            }
        }
    }
}

代码解读：

- 第一步：从结果中获取source。hit.getSourceAsString()，这部分是非高亮结果，json字符串。还需要反序列为HotelDoc对象
- 第二步：获取高亮结果。hit.getHighlightFields()，返回值是一个Map，key是高亮字段名称，值是HighlightField对象，代表高亮值
- 第三步：从map中根据高亮字段名称，获取高亮字段值对象HighlightField
- 第四步：从HighlightField中获取Fragments，并且转为字符串。这部分就是真正的高亮字符串了
- 第五步：用高亮的结果替换HotelDoc中的非高亮结果


*/

/*
# 数据聚合

聚合(aggregations)就是按照一定的规则将多个文档数据进行分类、加工(统计、分析、计算)[aggregation n.聚合,聚集]

## 聚合的种类

聚合常见的有三类：

- 桶聚合 Bucket Aggregation：用来对文档做分组
  - Term Aggregation 词条聚合：按照文档字段值分组，例如按照品牌值分组、按照国家分组
  - Date Histogram 日期柱状图：按照日期阶梯分组，例如一周为一组，或者一月为一组[histogram n.柱状图,直方图]

- 度量聚合 Metric Aggregation：用以计算一些值，比如：最大值、最小值、平均值等[metric n.度量标准 a.度量的]
  - Avg：求平均值
  - Max：求最大值
  - Min：求最小值
  - Stats：同时求max、min、avg、sum等[stats n.统计数据=statistic]

- 管道聚合 Pipeline Aggregation：以其它聚合的结果为基础做聚合[pipeline n.管道]


# 总结

什么是聚合？
按照一定的规则将多个文档数据进行分类、加工(统计、分析、计算)

聚合的常见种类有哪些？
Bucket Aggregation：对文档数据分组，并统计每组数量,例如:Term Aggregation 词条聚合
Metric Aggregation：对文档数据做计算，例如:Avg：求平均值
Pipeline Aggregation：基于其它聚合结果再做聚合

参与聚合的字段类型必须是：
参加聚合的字段必须是不能分词的,例如:keyword、日期、数值、布尔类型,而不能是text类型,因为分词后一个字段将会同时属于好几个分类无法确字段所属分类


-----------------------------------------------------------------------

## DSL实现聚合

现在，我们要统计所有数据中的酒店品牌有几种，其实就是按照品牌对数据分组。此时可以根据酒店品牌的名称做聚合，也就是Bucket聚合。

Bucket桶聚合语法

GET  /hotel/_search
{
    "size":  0,    //  设置size为0，结果中不包含文档, 即结果中hits:[]为空 ，只包含聚合结果
    "aggs":  {  //  定义聚合
        "brandAgg":  {  //给聚合起个名字
            "terms":  {  //  聚合的类型，按照品牌值聚合，所以选择terms aggregation
                "field":  "brand",  //  参与聚合的字段
                "size":  5  //  希望获取的聚合结果数量,默认是10
            }
        }
    }
}



 "hits" : {
    "total" : {
      "value" : 201,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "brandAggregation" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 96,
      "buckets" : [
        {
          "key" : "7天酒店",
          "doc_count" : 30
        },
        {
          "key" : "如家",
          "doc_count" : 30
        },
        {
          "key" : "皇冠假日",
          "doc_count" : 17
        },
        {
          "key" : "速8",
          "doc_count" : 15
        },
        {
          "key" : "万怡",
          "doc_count" : 13
        }
      ]
    }
  }



聚合结果排序

默认情况下，Bucket聚合会统计Bucket内的文档数量，记为_count，并且按照_count降序排序。
我们可以指定order属性，自定义聚合的排序方式：

GET /hotel/_search
{
  "size": 0,
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brand",
        "size": 20,
        "order": {
          "_count": "asc" // 按照_count升序排列
        }
      }
    }
  }
}


限定聚合范围

默认情况下，Bucket聚合是对索引库的所有文档做聚合，但真实场景下，用户会输入搜索条件，因此聚合必须是对搜索结果聚合。那么聚合必须添加限定条件。

我们可以限定要聚合的文档范围，只要添加query条件即可：

GET /hotel/_search
{
  "query": {
    "range": {
      "price": {
        "lte": 200 // 只对200元以下的文档聚合
      }
    }
  }, 
  "size": 0, 
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brand",
        "size": 20
      }
    }
  }
}


---------------------------------------------------------
Metric度量聚合

我们对酒店按照品牌分组，形成了一个个桶。现在我们需要对桶内的酒店做运算，获取每个品牌的用户评分的min、max、avg等值。

这就要用到Metric聚合了，例如stats聚合：就可以同时获取min、max、avg等结果。
因为我们需要在每个桶分别计算,所以这里的score_stats聚合是在brandAgg的聚合内部嵌套的子聚合。

语法如下：

GET /hotel/_search
{
  "size": 0, 
  "aggs": {
    "brandAgg": { 
      "terms": { 
        "field": "brand", 
        "size": 20,
        "order": {
          "scoreAgg.avg": "desc" //另外，我们还可以给聚合结果做个排序，例如按照每个桶的酒店平均分做排序
        }
      },
      "aggs": { // 是brands聚合的子聚合，也就是分组后对每组分别计算
        "scoreAgg": { // 聚合名称
          "stats": { // 聚合类型，这  里stats可以同时计算min、max、avg等
            "field": "score" // 聚合字段，这里是score
          }
        }
      }
    }
  }
}

-----------------------------------------------------

# 连用多个聚合

GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "size": 0,
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brand",
        "size": 5
      }
    },
    "cityAgg": {
      "terms": {
        "field": "city",
        "size": 5
      }
    },
    "starNameAgg": {
      "terms": {
        "field": "starName",
        "size": 5
      }
    }
  }
}

结果如下:
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 201,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "starNameAgg" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 7,
      "buckets" : [
        {
          "key" : "二钻",
          "doc_count" : 84
        },
        {
          "key" : "五钻",
          "doc_count" : 49
        },
        {
          "key" : "四钻",
          "doc_count" : 28
        },
        {
          "key" : "五星级",
          "doc_count" : 20
        },
        {
          "key" : "三钻",
          "doc_count" : 13
        }
      ]
    },
    "cityAgg" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "上海",
          "doc_count" : 83
        },
        {
          "key" : "北京",
          "doc_count" : 62
        },
        {
          "key" : "深圳",
          "doc_count" : 56
        }
      ]
    },
    "brandAgg" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 96,
      "buckets" : [
        {
          "key" : "7天酒店",
          "doc_count" : 30
        },
        {
          "key" : "如家",
          "doc_count" : 30
        },
        {
          "key" : "皇冠假日",
          "doc_count" : 17
        },
        {
          "key" : "速8",
          "doc_count" : 15
        },
        {
          "key" : "万怡",
          "doc_count" : 13
        }
      ]
    }
  }
}
可见当连用多个聚合时,每次都是对原始查询结果进行聚合,然后聚合结果平行排列在"aggregations" : {}中,而不是后一次对前一次的结果再聚合
要形成嵌套聚合,需要在一次聚合中进行下一次子聚合
"aggs": {
    "brandAgg": {// 聚合名称
        "terms": {// 聚合类型
            field": "brand"// 聚合字段
         },
         "aggs": {
            "scoreAgg": {// 聚合名称
                "stats": { // 聚合类型
                    "field": "score"// 聚合字段
                }
            }
         }
    }
}

-----------------------------------------------------

# 总结

aggs代表聚合，与query同级，此时query的作用是？
限定聚合的的文档范围,先查询,再对查询结果聚合

聚合必须的三要素：
 "aggs": {
    "NAME": {
      "AGG_TYPE": {
          "FIELD": "xxx"
      }
    }
  }
1.聚合名称 NAME
2.聚合类型 AGG_TYPE
3.聚合字段 FIELD

聚合可配置属性有：
field：指定聚合字段
size：指定聚合结果数量,默认为10条
order：指定聚合结果排序方式

-----------------------------------------------------

RestHighLevelClient实现聚合

聚合条件与query条件同级别，因此需要使用request.source()来指定聚合条件。
聚合的结果也与查询结果不同，API也比较特殊。不过同样是JSON逐层解析即可

Java代码:
 @Test
    void testAggregation() throws IOException {
        //1.准备Request
        SearchRequest searchRequest = new SearchRequest("hotel");
        //2.准备DSL

        DSL请求格式:
        GET /hotel/_search
        {
          "size": 0,
          "aggs": {
            "brandAgg": {
              "terms": {
                "field": "brand",
                "size": 10
              }
            }
          }
        }

        //2.1设置size
        searchRequest.source().size(0);
        //2.2聚合
        searchRequest.source().aggregation(AggregationBuilders
                .terms("brandAgg")//聚合类型、聚合名称
                .field("brand")//聚合字段
                .size(10));//指定聚合结果数量,默认为10条

        //3.发出请求
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        //4.解析结果

      结果json格式:
        {
          "took" : 2,
          "timed_out" : false,
          "_shards" : {
            "total" : 1,
            "successful" : 1,
            "skipped" : 0,
            "failed" : 0
          },
          "hits" : {
            "total" : {
              "value" : 201,
              "relation" : "eq"
            },
            "max_score" : null,
            "hits" : [ ]
          },
          "aggregations" : {
            "brandAgg" : {
              "doc_count_error_upper_bound" : -1,
              "sum_other_doc_count" : 179,
              "buckets" : [
                {
                  "key" : "万丽",
                  "doc_count" : 2,
                  "scoreAgg" : {
                    "count" : 2,
                    "min" : 46.0,
                    "max" : 47.0,
                    "avg" : 46.5,
                    "sum" : 93.0
                  }
                },
                {
                  "key" : "凯悦",
                  "doc_count" : 8,
                  "scoreAgg" : {
                    "count" : 8,
                    "min" : 45.0,
                    "max" : 47.0,
                    "avg" : 46.25,
                    "sum" : 370.0
                  }
                },
                {
                  "key" : "和颐",
                  "doc_count" : 12,
                  "scoreAgg" : {
                    "count" : 12,
                    "min" : 44.0,
                    "max" : 47.0,
                    "avg" : 46.083333333333336,
                    "sum" : 553.0
                  }
                }
              ]
            }
          }
        }

    Aggregations aggregations = searchResponse.getAggregations();
    //4.1 根据聚合名称获取聚合结果
    Terms brandTerms = aggregations.get("brandAgg");
    //4.2 获取buckets
    List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
    //4.3 遍历
        for (Terms.Bucket bucket : buckets) {
        //4.4 获取分类关键字key
        String key = bucket.getKeyAsString();
        System.err.println(key);
        //7天酒店
        //如家
        //皇冠假日
        //速8
        //万怡
        //华美达
        //和颐
        //万豪
        //喜来登
        //希尔顿
    }
}

 */



/*
自动补全



当用户在搜索框输入字符时，我们应该提示出与该字符有关的搜索项,这种根据用户输入的字母，提示完整词条的功能，就是自动补全了。


要实现根据字母做补全，就必须对文档按照拼音分词。在GitHub上恰好有elasticsearch的拼音分词插件。地址：https://github.com/medcl/elasticsearch-analysis-pinyin


安装方式与IK分词器一样，分三步：
①解压
②上传到虚拟机中，elasticsearch的plugin目录
③重启elasticsearch
④测试


测试拼音分词器:

POST /_analyze
{
  "text": "如家酒店还不错",
  "analyzer": "pinyin"
}


## 自定义分词器
默认的拼音分词器会将每个汉字单独分为拼音，而我们希望的是每个词条形成一组拼音，需要对拼音分词器做个性化定制，形成自定义分词器。

elasticsearch中分词器（analyzer）的组成包含可选的三部分：

- character filters：在tokenizer之前对文本进行处理。例如删除字符、替换字符
- tokenizer：将文本按照一定的规则切割成词条（term）。例如keyword，就是不分词；还有ik_smart
- tokenizer filter：将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等

于是可以在tokenizer使用ik分词器分词,在tokenizer filter使用pinyin分词器转换成拼音,就实现了按照词条转换拼音


# 自定义分词器
PUT /test # 创建一个test索引库的同时在settings中自定义分词器,并在定义mappings映射关系时应用自定义分词器
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_analyzer": {
          "tokenizer": "ik_max_word",
          "filter": "py" # 自定义的分词模式规则
        }
      },
      "filter": { # 自定义的分词模式规则
        "py": {
          "type": "pinyin",
          "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "limit_first_letter_length": 16,
          "remove_duplicated_term": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "CityName":{
        "type": "text",
        "analyzer": "my_analyzer", # 指定创建倒排索引时的分词器为上面自定义的分词器
        "search_analyzer": "ik_max_word" #指定搜索时的分词器为ik分词器,这样不会出现搜索时因为分词后拼音相同而搜索到不相干的中文同音字
      }
    }
  }
}

#测试自定义分词器
POST /test/_analyze
{
  "text": ["如家酒店还不错!"],
  "analyzer": "my_analyzer"
}



## 自动补全查询

elasticsearch提供了Completion Suggester(https://www.elastic.co/guide/en/elasticsearch/reference/7.6/search-suggesters.html)查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率，对于文档中字段的类型有一些约束：

- 参与补全查询的字段必须是completion类型。
- 字段的内容一般是用来补全的多个词条形成的数组。

# 创建自动补全的索引库
PUT /test
{
  "mappings": {
    "properties": {
      "title":{
        "type": "completion"
      }
    }
  }
}

# 添加示例数据
POST test/_doc
{
  "title": ["Sony", "WH-1000XM3"]
}
POST test/_doc
{
  "title": ["SK-II", "PITERA"]
}
POST test/_doc
{
  "title": ["Nintendo", "switch"]
}

# 自动补全查询
GET /test/_search
{
  "suggest": {
    "MY_TITLE_SUGGESTION": { # 自动补全查询名称
      "text": "s", # 关键字
      "completion": {
        "field": "title", # 补全查询的字段
        "skip_duplicates": true, # 跳过重复的
        "size": 10 # 显示前10条结果
      }
    }
  }
}

结果:
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "suggest" : {
    "MY_TITLE_SUGGESTION" : [
      {
        "text" : "s",
        "offset" : 0,
        "length" : 1,
        "options" : [
          {
            "text" : "SK-II",
            "_index" : "test",
            "_type" : "_doc",
            "_id" : "KPkYq4gB8rG8FMDTPLk-",
            "_score" : 1.0,
            "_source" : {
              "title" : [
                "SK-II",
                "PITERA"
              ]
            }
          },
          {
            "text" : "Sony",
            "_index" : "test",
            "_type" : "_doc",
            "_id" : "J_kYq4gB8rG8FMDTMrl6",
            "_score" : 1.0,
            "_source" : {
              "title" : [
                "Sony",
                "WH-1000XM3"
              ]
            }
          },
          {
            "text" : "switch",
            "_index" : "test",
            "_type" : "_doc",
            "_id" : "KfkYq4gB8rG8FMDTRblz",
            "_score" : 1.0,
            "_source" : {
              "title" : [
                "Nintendo",
                "switch"
              ]
            }
          }
        ]
      }
    ]
  }
}



---------------------------------------------------------------
实现酒店搜索框自动补全

现在，我们的hotel索引库还没有设置拼音分词器，需要修改索引库中的配置。但是我们知道索引库是无法修改的，只能删除然后重新创建。

另外，我们需要添加一个字段，用来做自动补全，将brand,business都放进去，作为自动补全的提示。

因此，总结一下，我们需要做的事情包括：

1. 修改hotel索引库结构，设置自定义拼音分词器
2. 修改索引库的name、all字段，使用自定义分词器
3. 索引库添加一个新字段suggestion，类型为completion类型，使用自定义的分词器
4. 给HotelDoc类添加suggestion字段，内容包含brand、business
5. 重新导入数据到hotel库



### 修改酒店映射结构
代码如下：
// 酒店数据索引库
PUT /hotel
{
  "settings": {
    "analysis": {
      "analyzer": {
        "text_anlyzer": {
          "tokenizer": "ik_max_word",
          "filter": "py"
        },
        "completion_analyzer": {
          "tokenizer": "keyword",
          "filter": "py"
        }
      },
      "filter": {
        "py": {
          "type": "pinyin",
          "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "limit_first_letter_length": 16,
          "remove_duplicated_term": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "id":{
        "type": "keyword"
      },
      "name":{
        "type": "text",
        "analyzer": "text_anlyzer",#指定创建索引时使用自定义的text_anlyzer分词器
        "search_analyzer": "ik_smart",#指定搜索切分搜索条件时使用ik_smart分词器
        "copy_to": "all"
      },
      "address":{
        "type": "keyword",
        "index": false
      },
      "price":{
        "type": "integer"
      },
      "score":{
        "type": "integer"
      },
      "brand":{
        "type": "keyword",
        "copy_to": "all"
      },
      "city":{
        "type": "keyword"
      },
      "starName":{
        "type": "keyword"
      },
      "business":{
        "type": "keyword",
        "copy_to": "all"
      },
      "location":{
        "type": "geo_point"
      },
      "pic":{
        "type": "keyword",
        "index": false
      },
      "all":{
        "type": "text",
        "analyzer": "text_anlyzer", #指定创建索引时使用自定义的text_anlyzer分词器
        "search_analyzer": "ik_smart"#指定搜索切分搜索条件时使用ik_smart分词器
      },
      "suggestion":{
          "type": "completion",
          "analyzer": "completion_analyzer" #指定创建索引时使用自定义的completion_analyzer分词器
      }
    }
  }
}


### 修改HotelDoc实体

HotelDoc中要添加一个字段，用来做自动补全，内容可以是酒店品牌、城市、商圈等信息。按照自动补全字段的要求，最好是这些字段的数组。

因此我们在HotelDoc中添加一个suggestion字段，类型为`List<String>`，然后将brand,business放到里面。

代码如下：
@Data
@NoArgsConstructor
public class HotelDoc {//用于数据库行类型 转换为 索引库文档类型
    private Long id;
    private String name;
    private String address;
    private Integer price;
    private Integer score;
    private String brand;
    private String city;
    private String starName;
    private String business;
    private String location;
    private String pic;

    private Object distance;
    private Boolean isAD;

    private List<String> suggestion;←←←←←←

    public HotelDoc(Hotel hotel) {
        this.id = hotel.getId();
        this.name = hotel.getName();
        this.address = hotel.getAddress();
        this.price = hotel.getPrice();
        this.score = hotel.getScore();
        this.brand = hotel.getBrand();
        this.city = hotel.getCity();
        this.starName = hotel.getStarName();
        this.business = hotel.getBusiness();
        this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
        this.pic = hotel.getPic();

        if (this.business.contains("、")) { ←←←←←←
            //business有多个值,需要切割
            String[] arr = this.business.split("、");
            this.suggestion = new ArrayList<>();
            this.suggestion.add(this.brand);
            Collections.addAll(this.suggestion,arr);
        } else if (this.business.contains("/")) {
            //business有多个值,需要切割
            String[] arr = this.business.split("/");
            this.suggestion = new ArrayList<>();
            this.suggestion.add(this.brand);
            Collections.addAll(this.suggestion,arr);
        } else{
            this.suggestion = Arrays.asList(this.brand, this.business);
        }

    }
}


### 重新导入
重新执行之前编写的导入数据功能，可以看到新的酒店数据中包含了suggestion字段

# 测试带拼音索引的自动补全查询功能
GET /hotel/_search
{
  "suggest": {
    "YOUR_SUGGESTION": {
      "text": "sd",
      "completion": {
        "field": "suggestion",
        "skip_duplicates": true,
        "size": 10
      }
    }
  }
}



 */
}
